1 /** 2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This file contains the command line parsing and configuration loading from file. 7 8 The data flow is to first load the configuration from the file then parse the command line. 9 This allows the user to override the configuration via the CLI. 10 */ 11 module code_checker.cli; 12 13 import logger = std.experimental.logger; 14 import std.array : array, empty, appender; 15 import std.exception : collectException, ifThrown; 16 import std.path : buildPath, dirName; 17 import std.typecons : Tuple, Flag; 18 19 import toml : TOMLDocument, TOMLValue; 20 import my.path : AbsolutePath, Path; 21 22 @safe: 23 24 enum AppMode { 25 none, 26 help, 27 helpUnknownCommand, 28 normal, 29 initConfig, 30 } 31 32 /// Configuration options only relevant for static code checkers. 33 struct ConfigStaticCode { 34 import code_checker.engine.types : Severity; 35 36 /// Filter results from analyzers on this severity. 37 Severity severity; 38 39 /// Analyzers to use. 40 string[] analyzers = ["clang-tidy"]; 41 42 /// Files matching this pattern should not be analyzed. 43 string[] fileExcludeFilter; 44 } 45 46 /// Configuration options only relevant for clang-tidy. 47 struct ConfigClangTidy { 48 /// System configuration to use if .clang-tidy do not exists in work directory. 49 string systemConfig = "{code_checker}/../etc/code_checker/clang_tidy.conf"; 50 51 /// Checks to toggle on/off. Used as a compliment to checks. 52 string[] checkExtensions; 53 54 /// Used as a compliment to options. options is set in the global while optionExtensions is set locally. 55 string[string] optionExtensions; 56 57 /// Argument to the be passed on to clang-tidy's --header-filter paramter as-is 58 string headerFilter; 59 60 /// Apply fix hints. 61 bool applyFixit; 62 63 /// Apply fix hints even though they result in errors. 64 bool applyFixitErrors; 65 66 /// The clang-tidy binary to use. 67 string binary = "clang-tidy"; 68 } 69 70 /// Configuration options only relevant for iwyu. 71 struct ConfigIwyu { 72 /// The clang-tidy binary to use. 73 string binary = "iwyu"; 74 75 /// Extra args to pass on to iwyu. 76 string[] extraFlags; 77 78 /// Map files to pass on to iwyu. 79 string[] maps; 80 81 /// Map files to pass on to iwyu. 82 string[] defaultMaps; 83 } 84 85 /// Configuration data for the compile_commands.json 86 struct ConfigCompileDb { 87 import compile_db : CompileCommandFilter; 88 89 /// Command to generate the compile_commands.json 90 string generateDb; 91 92 /// Dependencies that the generate command have. If it is set then the 93 /// commmand is only executed when they are changed. 94 AbsolutePath[] generateDbDeps; 95 96 /// Raw user input via either config or cli 97 string[] rawDbs; 98 99 /// Either a path to a compilation database or a directory to search for one in. 100 AbsolutePath[] dbs; 101 102 /// Flags the user wants to be automatically removed from the compile_commands.json. 103 CompileCommandFilter flagFilter; 104 105 /// If files should be deduplicated thus only unique files are analyzed. 106 bool dedupFiles; 107 } 108 109 /// Settings for the compiler 110 struct Compiler { 111 import compile_db : SystemCompiler = Compiler; 112 113 /// Additional flags the user wants to add besides those that are in the compile_commands.json. 114 string[] extraFlags; 115 116 /// Deduce compiler flags from this compiler and not the one in the 117 /// supplied compilation database. / This is needed when the one specified 118 /// in the DB has e.g. a c++ stdlib that is not compatible with clang. 119 SystemCompiler useCompilerSystemIncludes; 120 } 121 122 /// Settings for logging. 123 struct Logging { 124 import colorlog : VerboseMode; 125 126 VerboseMode verbose; 127 128 /// If logging to files should be done. 129 bool toFile; 130 131 /// Directory to log to. 132 AbsolutePath dir; 133 } 134 135 /// Configuration of how to use the program. 136 struct Config { 137 AppMode mode; 138 139 /// Where the base configurations are stored. 140 AbsolutePath baseConfDir; 141 142 /// System configuration. 143 AbsolutePath systemConfDir; 144 145 /// Name of the base configuration to merge with the users. 146 string baseConfName = "default"; 147 148 /// Path to the base configuration that the user wants to use. 149 AbsolutePath baseUserConf() @safe const { 150 return buildPath(baseConfDir, "code_checker_" ~ baseConfName ~ ".toml").Path.AbsolutePath; 151 } 152 153 /// Working directory as specified by the user. 154 AbsolutePath workDir; 155 156 /// Configuration file as specified by the user or the default one. 157 AbsolutePath confFile; 158 159 AbsolutePath database; 160 161 ConfigClangTidy clangTidy; 162 ConfigCompileDb compileDb; 163 ConfigIwyu iwyu; 164 ConfigStaticCode staticCode; 165 166 Compiler compiler; 167 Logging logg; 168 169 /// If set then only analyze these files. 170 AbsolutePath[] analyzeFiles; 171 172 /// Returns: a config object with default values. 173 static Config make(AbsolutePath workDir, AbsolutePath confFile) @safe { 174 import std.file : thisExePath; 175 import std.process : environment; 176 import compile_db : defaultCompilerFlagFilter, CompileCommandFilter; 177 178 Config c; 179 c.workDir = workDir; 180 c.confFile = confFile; 181 c.compileDb.flagFilter = CompileCommandFilter(defaultCompilerFlagFilter, 0); 182 c.baseConfDir = environment.get("CODE_CHECKER_DEFAULT", 183 buildPath(thisExePath.dirName, "..")).Path.AbsolutePath; 184 c.systemConfDir = AbsolutePath(c.baseConfDir ~ "etc/code_checker"); 185 186 return c; 187 } 188 } 189 190 /// Minimal config to setup path to config file and workdir. 191 struct MiniConfig { 192 /// Value from the user via CLI, unmodified. 193 string rawWorkDir; 194 195 /// Converted to an absolute path. 196 AbsolutePath workDir; 197 198 /// Value from the user via CLI, unmodified. 199 string rawConfFile = ".code_checker.toml"; 200 201 /// The configuration file that has been loaded 202 AbsolutePath confFile; 203 } 204 205 /// Returns: minimal config to load settings and setup working directory. 206 MiniConfig parseConfigCLI(string[] args) @trusted nothrow { 207 import std.file : getcwd; 208 import std.path : dirName; 209 static import std.getopt; 210 211 MiniConfig conf; 212 213 try { 214 std.getopt.getopt(args, std.getopt.config.keepEndOfOptions, std.getopt.config.passThrough, 215 "workdir", "none not visible to the user", &conf.rawWorkDir, 216 "c|config", "none not visible to the user", &conf.rawConfFile); 217 conf.confFile = Path(conf.rawConfFile).AbsolutePath; 218 if (conf.rawWorkDir.length == 0) { 219 conf.rawWorkDir = getcwd; 220 } 221 conf.workDir = Path(conf.rawWorkDir).AbsolutePath; 222 } catch (Exception e) { 223 logger.error("Invalid cli values: ", e.msg).collectException; 224 logger.trace(conf).collectException; 225 } 226 227 return conf; 228 } 229 230 void parseCLI(string[] args, ref Config conf) @trusted { 231 import std.algorithm : map, among, filter; 232 import std.format : format; 233 import std.path : dirName, buildPath; 234 import std.traits : EnumMembers; 235 import code_checker.engine.types : Severity; 236 import colorlog : VerboseMode; 237 static import std.getopt; 238 239 bool verbose_info; 240 bool verbose_trace; 241 std.getopt.GetoptResult help_info; 242 try { 243 bool init_conf; 244 string config_file = ".code_checker.toml"; 245 string database = "code_checker.sqlite3"; 246 string logdir = "."; 247 string workdir; 248 string[] analyze_files; 249 string[] analyzers; 250 string[] compile_dbs; 251 string[] src_filter; 252 253 // dfmt off 254 help_info = std.getopt.getopt(args, 255 "a|analyzer", "Analysers to run", &analyzers, 256 "clang-tidy-bin", "clang-tidy binary to use", &conf.clangTidy.binary, 257 "clang-tidy-fix", "apply suggested clang-tidy fixes", &conf.clangTidy.applyFixit, 258 "clang-tidy-fix-errors", "apply suggested clang-tidy fixes even if they result in compilation errors", &conf.clangTidy.applyFixitErrors, 259 "compile-db", "path to a compilationi database or where to search for one", &compile_dbs, 260 "c|config", "load configuration (default: .code_checker.toml)", &config_file, 261 "db|database", "Database path", &database, 262 "f|file", "if set then analyze only these files (default: all)", &analyze_files, 263 "init", "create an initial config to use", &init_conf, 264 "init-template", "base the initial config on the named template (default: default)", &conf.baseConfName, 265 "iwyu-bin", "iwyu binary to use", &conf.iwyu.binary, 266 "iwyu-map", "give iwyu one or more mapping files", &conf.iwyu.maps, 267 "log", "create a logfile for each analyzed file", &conf.logg.toFile, 268 "logdir", "path to create logfiles in (default: .)", &logdir, 269 "severity", format("report issues with a severity >= to this value (default: style) %s", [EnumMembers!Severity]), &conf.staticCode.severity, 270 "v|verbose", format("verbose mode is set to trace (%-(%s,%))", [EnumMembers!VerboseMode]), &conf.logg.verbose, 271 "workdir", "use this path as the working directory when programs used by analyzers are executed (default: .)", &workdir, 272 ); 273 // dfmt on 274 conf.mode = AppMode.normal; 275 if (help_info.helpWanted) 276 conf.mode = AppMode.help; 277 else if (init_conf) 278 conf.mode = AppMode.initConfig; 279 280 // use a sane default which is to look in the current directory 281 if (compile_dbs.length == 0 && conf.compileDb.dbs.length == 0) { 282 compile_dbs = ["./compile_commands.json"]; 283 } else if (compile_dbs.length != 0) { 284 conf.compileDb.rawDbs = compile_dbs; 285 } 286 287 if (!analyzers.empty) 288 conf.staticCode.analyzers = analyzers; 289 290 if (conf.logg.toFile) 291 conf.logg.dir = Path(logdir).AbsolutePath; 292 293 conf.database = AbsolutePath(database); 294 295 // dfmt off 296 conf.compileDb.dbs = conf 297 .compileDb.rawDbs 298 .filter!(a => a.length != 0) 299 .map!(a => Path(buildPath(conf.workDir, a)).AbsolutePath) 300 .array; 301 // dfmt on 302 303 conf.analyzeFiles = analyze_files.map!(a => Path(buildPath(conf.workDir, 304 a)).AbsolutePath).array; 305 } catch (std.getopt.GetOptException e) { 306 // unknown option 307 logger.error(e.msg); 308 conf.mode = AppMode.helpUnknownCommand; 309 } catch (Exception e) { 310 logger.error(e.msg); 311 conf.mode = AppMode.helpUnknownCommand; 312 } 313 314 void printHelp() @trusted { 315 import std.getopt : defaultGetoptPrinter; 316 import std.format : format; 317 import std.path : baseName; 318 319 defaultGetoptPrinter(format("usage: %s\n", args[0].baseName), help_info.options); 320 } 321 322 if (conf.mode.among(AppMode.help, AppMode.helpUnknownCommand)) { 323 printHelp; 324 return; 325 } 326 } 327 328 /** Load the configuration from file. 329 * 330 * Example of a TOML configuration 331 * --- 332 * [defaults] 333 * check_name_standard = true 334 * --- 335 */ 336 void loadConfig(ref Config rval, string configFile) @trusted { 337 import std.algorithm : map; 338 import std.file : exists, readText; 339 import std.path : dirName, buildPath; 340 import toml; 341 342 if (!exists(configFile)) 343 return; 344 345 static auto tryLoading(string configFile) { 346 auto txt = readText(configFile); 347 auto doc = parseTOML(txt); 348 return doc; 349 } 350 351 TOMLDocument doc; 352 try { 353 doc = tryLoading(configFile); 354 } catch (Exception e) { 355 logger.warning("Unable to read the configuration from ", configFile); 356 logger.warning(e.msg); 357 return; 358 } 359 360 alias Fn = void delegate(ref Config c, ref TOMLValue v); 361 Fn[string] callbacks; 362 363 void defaults__check_name_standard(ref Config c, ref TOMLValue v) { 364 import std.traits : EnumMembers; 365 import code_checker.engine.types : toSeverity, Severity; 366 367 auto s = toSeverity(v.str); 368 if (s.isNull) { 369 logger.warningf("Unknown severity level %s. Using default: style", v.str); 370 logger.warningf("valid values are: %s", [EnumMembers!Severity]); 371 c.staticCode.severity = Severity.style; 372 } else { 373 c.staticCode.severity = s.get; 374 } 375 } 376 377 callbacks["defaults.severity"] = &defaults__check_name_standard; 378 callbacks["defaults.analyzers"] = (ref Config c, ref TOMLValue v) { 379 c.staticCode.analyzers = v.array.map!"a.str".array; 380 }; 381 382 callbacks["compile_commands.search_paths"] = (ref Config c, ref TOMLValue v) { 383 c.compileDb.rawDbs = v.array.map!"a.str".array; 384 }; 385 callbacks["compile_commands.generate_cmd"] = (ref Config c, ref TOMLValue v) { 386 c.compileDb.generateDb = v.str; 387 }; 388 callbacks["compile_commands.generate_cmd_deps"] = (ref Config c, ref TOMLValue v) { 389 try { 390 c.compileDb.generateDbDeps = v.array 391 .map!"a.str" 392 .map!(a => AbsolutePath(a)) 393 .array; 394 } catch (Exception e) { 395 logger.warning(e.msg); 396 } 397 }; 398 callbacks["compile_commands.exclude"] = (ref Config c, ref TOMLValue v) { 399 c.staticCode.fileExcludeFilter = v.array.map!"a.str".array; 400 }; 401 callbacks["compile_commands.filter"] = (ref Config c, ref TOMLValue v) { 402 import compile_db : FilterClangFlag; 403 404 c.compileDb.flagFilter.filter = v.array.map!(a => FilterClangFlag(a.str)).array; 405 }; 406 callbacks["compile_commands.skip_compiler_args"] = (ref Config c, ref TOMLValue v) { 407 c.compileDb.flagFilter.skipCompilerArgs = cast(int) v.integer; 408 }; 409 callbacks["compile_commands.dedup"] = (ref Config c, ref TOMLValue v) { 410 c.compileDb.dedupFiles = v == true; 411 }; 412 413 callbacks["clang_tidy.binary"] = (ref Config c, ref TOMLValue v) { 414 c.clangTidy.binary = v.str; 415 }; 416 callbacks["clang_tidy.header_filter"] = (ref Config c, ref TOMLValue v) { 417 c.clangTidy.headerFilter = v.str; 418 }; 419 callbacks["clang_tidy.checks"] = (ref Config c, ref TOMLValue v) { 420 logger.warning("clang_tidy.checks is deprecated. It is replaced by ", 421 c.clangTidy.systemConfig); 422 }; 423 callbacks["clang_tidy.check_extensions"] = (ref Config c, ref TOMLValue v) { 424 c.clangTidy.checkExtensions = v.array.map!(a => a.str).array; 425 }; 426 callbacks["clang_tidy.options"] = (ref Config c, ref TOMLValue v) { 427 logger.warning("clang_tidy.options is deprecated. It is replaced by ", 428 c.clangTidy.systemConfig); 429 }; 430 callbacks["clang_tidy.option_extensions"] = (ref Config c, ref TOMLValue v) { 431 // dummo to suppress warning about unknown key 432 }; 433 callbacks["clang_tidy.system_config"] = (ref Config c, ref TOMLValue v) { 434 c.clangTidy.systemConfig = v.str; 435 }; 436 437 callbacks["compiler.extra_flags"] = (ref Config c, ref TOMLValue v) { 438 c.compiler.extraFlags = v.array.map!(a => a.str).array; 439 }; 440 callbacks["compiler.use_compiler_system_includes"] = (ref Config c, ref TOMLValue v) { 441 c.compiler.useCompilerSystemIncludes = v.str; 442 }; 443 444 callbacks["iwyu.binary"] = (ref Config c, ref TOMLValue v) { 445 c.iwyu.binary = v.str; 446 }; 447 callbacks["iwyu.flags"] = (ref Config c, ref TOMLValue v) { 448 c.iwyu.extraFlags = v.array.map!(a => a.str).array; 449 }; 450 callbacks["iwyu.mapping_files"] = (ref Config c, ref TOMLValue v) { 451 c.iwyu.maps = v.array.map!(a => a.str).array; 452 }; 453 callbacks["iwyu.default_mapping_files"] = (ref Config c, ref TOMLValue v) { 454 c.iwyu.defaultMaps = v.array.map!(a => a.str).array; 455 }; 456 457 void iterSection(ref Config c, string sectionName) { 458 if (auto section = sectionName in doc) { 459 // specific configuration from section members 460 foreach (k, v; *section) { 461 if (auto cb = sectionName ~ "." ~ k in callbacks) 462 (*cb)(c, v); 463 else 464 logger.infof("Unknown key '%s' in configuration section '%s'", k, sectionName); 465 } 466 } 467 } 468 469 iterSection(rval, "defaults"); 470 iterSection(rval, "compile_commands"); 471 iterSection(rval, "compiler"); 472 iterSection(rval, "clang_tidy"); 473 iterSection(rval, "iwyu"); 474 475 if (auto section = "clang_tidy" in doc) 476 rval.clangTidy.optionExtensions = parseDict(*section, "option_extensions"); 477 } 478 479 string[string] parseDict(ref TOMLValue root, string section) @trusted { 480 import toml; 481 482 typeof(return) rval; 483 484 if (section !in root) 485 return rval; 486 487 foreach (k, s; *(section in root)) { 488 try { 489 rval[k] = s.str; 490 } catch (Exception e) { 491 logger.warningf("error in %s: %s", section, e.msg); 492 } 493 } 494 495 return rval; 496 }